-
Notifications
You must be signed in to change notification settings - Fork 0
feat(cli): add interactive TUI dashboard #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Add terminal-based dashboard for FGP daemon monitoring using Ratatui: - Service list with real-time status (running/stopped/unhealthy/error) - Keyboard navigation (vim keys + arrows) - Start/stop service actions - Auto-refresh with configurable polling interval - Help overlay toggle - Success/error message feedback Usage: fgp tui [--poll <ms>] Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds an interactive terminal-based UI (TUI) dashboard for monitoring FGP daemon services using the Ratatui framework. The dashboard provides real-time service status monitoring with color-coded indicators, vim-style keyboard navigation, and service control capabilities (start/stop).
Changes:
- Added TUI dashboard with real-time service monitoring and control
- Implemented keyboard navigation with vim-style bindings and service management actions
- Added auto-refresh with configurable polling interval (default 2000ms)
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| src/tui/ui.rs | UI rendering logic including header, service table, footer with keybindings, and help overlay |
| src/tui/mod.rs | Main TUI entry point and event loop handling keyboard input and refresh events |
| src/tui/event.rs | Event handling system using threads for input, UI ticks, and service polling |
| src/tui/app.rs | Application state management, service discovery, and status checking logic |
| src/main.rs | Added TUI command to CLI enum |
| src/commands/tui.rs | TUI command implementation that delegates to tui module |
| src/commands/mod.rs | Added tui module declaration |
| Cargo.toml | Added ratatui, crossterm dependencies and tokio (unused) |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # Async runtime for TUI events | ||
| tokio = { version = "1", features = ["sync", "time", "rt-multi-thread"] } | ||
|
|
Copilot
AI
Jan 15, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The tokio dependency is added but not used anywhere in the TUI implementation. The event handling uses std::thread and std::sync::mpsc instead of async. This dependency should be removed to avoid unnecessary bloat.
| # Async runtime for TUI events | |
| tokio = { version = "1", features = ["sync", "time", "rt-multi-thread"] } |
| pub fn start_selected(&mut self) { | ||
| if let Some(service) = self.selected_service().cloned() { | ||
| if service.status == ServiceStatus::Stopped || service.status == ServiceStatus::Error { | ||
| match fgp_daemon::lifecycle::start_service(&service.name) { |
Copilot
AI
Jan 15, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code references fgp_daemon::lifecycle::start_service but this module doesn't exist. Other commands in the codebase use commands::start::run directly. This should likely use the existing commands module or duplicate the start logic here, similar to how other commands work.
| match fgp_daemon::lifecycle::start_service(&service.name) { | |
| match commands::start::run(&service.name) { |
| if let Some(service) = self.selected_service().cloned() { | ||
| if service.status == ServiceStatus::Running || service.status == ServiceStatus::Unhealthy | ||
| { | ||
| match fgp_daemon::lifecycle::stop_service(&service.name) { |
Copilot
AI
Jan 15, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code references fgp_daemon::lifecycle::stop_service but this module doesn't exist. Other commands use commands::stop::run directly. This should use the existing commands module or implement the stop logic similar to other commands.
| match fgp_daemon::lifecycle::stop_service(&service.name) { | |
| match commands::stop::run(&service.name) { |
|
|
||
| /// Discover all installed services. | ||
| fn discover_services() -> Vec<ServiceInfo> { | ||
| let services_dir = fgp_daemon::lifecycle::fgp_services_dir(); |
Copilot
AI
Jan 15, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code references fgp_daemon::lifecycle::fgp_services_dir() but this function doesn't exist in that module. The existing commands use commands::fgp_services_dir() from src/commands/mod.rs. Use the existing function instead.
| let services_dir = fgp_daemon::lifecycle::fgp_services_dir(); | |
| let services_dir = commands::fgp_services_dir(); |
| None => continue, | ||
| }; | ||
|
|
||
| let socket_path = fgp_daemon::lifecycle::service_socket_path(&name); |
Copilot
AI
Jan 15, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code references fgp_daemon::lifecycle::service_socket_path() but this function doesn't exist. The existing commands use commands::service_socket_path() from src/commands/mod.rs. Use the existing function instead.
| let socket_path = fgp_daemon::lifecycle::service_socket_path(&name); | |
| let socket_path = crate::commands::service_socket_path(&name); |
| let title = Line::from(vec![ | ||
| Span::styled( | ||
| " FGP Dashboard ", | ||
| Style::default() | ||
| .fg(Color::Cyan) | ||
| .add_modifier(Modifier::BOLD), | ||
| ), | ||
| Span::raw(" "), | ||
| Span::styled( | ||
| format!("Updated: {} ", time_str), |
Copilot
AI
Jan 15, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hard-coded whitespace for spacing is fragile and won't adapt to different terminal widths or content lengths. Consider using Layout constraints or calculating spacing dynamically based on available width.
| let title = Line::from(vec![ | |
| Span::styled( | |
| " FGP Dashboard ", | |
| Style::default() | |
| .fg(Color::Cyan) | |
| .add_modifier(Modifier::BOLD), | |
| ), | |
| Span::raw(" "), | |
| Span::styled( | |
| format!("Updated: {} ", time_str), | |
| let left_label = " FGP Dashboard "; | |
| let right_label = format!("Updated: {} ", time_str); | |
| let left_width = left_label.len() as u16; | |
| let right_width = right_label.len() as u16; | |
| let available_width = area.width; | |
| let padding_width = available_width.saturating_sub(left_width + right_width); | |
| let mut padding = String::new(); | |
| padding.reserve(padding_width as usize); | |
| for _ in 0..padding_width { | |
| padding.push(' '); | |
| } | |
| let title = Line::from(vec![ | |
| Span::styled( | |
| left_label, | |
| Style::default() | |
| .fg(Color::Cyan) | |
| .add_modifier(Modifier::BOLD), | |
| ), | |
| Span::raw(padding), | |
| Span::styled( | |
| right_label, |
| if secs < 60 { | ||
| format!("{}s", secs) | ||
| } else if secs < 3600 { | ||
| format!("{}m {}s", secs / 60, secs % 60) | ||
| } else if secs < 86400 { | ||
| format!("{}h {}m", secs / 3600, (secs % 3600) / 60) | ||
| } else { | ||
| format!("{}d {}h", secs / 86400, (secs % 86400) / 3600) |
Copilot
AI
Jan 15, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This format_uptime function is duplicated from src/commands/status.rs (lines 137-147). Consider extracting this to a shared utility module to avoid code duplication.
| if secs < 60 { | |
| format!("{}s", secs) | |
| } else if secs < 3600 { | |
| format!("{}m {}s", secs / 60, secs % 60) | |
| } else if secs < 86400 { | |
| format!("{}h {}m", secs / 3600, (secs % 3600) / 60) | |
| } else { | |
| format!("{}d {}h", secs / 86400, (secs % 86400) / 3600) | |
| let days = secs / 86_400; | |
| let rem_after_days = secs % 86_400; | |
| let hours = rem_after_days / 3_600; | |
| let rem_after_hours = rem_after_days % 3_600; | |
| let minutes = rem_after_hours / 60; | |
| let seconds = rem_after_hours % 60; | |
| match (days, hours, minutes) { | |
| (0, 0, 0) => format!("{}s", seconds), | |
| (0, 0, _) => format!("{}m {}s", minutes, seconds), | |
| (0, _, _) => format!("{}h {}m", hours, minutes), | |
| _ => format!("{}d {}h", days, hours), |
Summary
Features
↑/k↓/js/Enterxr?q/EscUsage
Test plan
fgp tuiand verify service list appears?qandEsc🤖 Generated with Claude Code